﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using Microsoft.WowAddonStudio.FrameXml.Components.Layout;
using Microsoft.WowAddonStudio.FrameXml.Components.Support;
using Microsoft.WowAddonStudio.FrameXml.Serialization;

namespace Microsoft.WowAddonStudio.FrameXml.Components
{
	public partial class LayoutFrameControl
	{
        private readonly Dictionary<AxisPoint, int> horizontalPoints = new Dictionary<AxisPoint, int>(3);
        private readonly Dictionary<AxisPoint, int> verticalPoints = new Dictionary<AxisPoint, int>(3);

        private bool suspendLayoutEvents = true;

        /// <summary>
        /// Performs the laying out of the frame control.
        /// </summary>
        /// <param name="affectedProperty">The affected property that triggered the layout.</param>
        public new void Layout(string affectedProperty)
        {
            suspendLayoutEvents = true;

            // Clear the layout before setting the frame points
            this.ClearLayout();

            if (LayoutFrame.Virtual)
            {
                SetVirtualLayoutPosition();
            }
            else
            {
                if (LayoutFrame.Anchors != null)
                {
                    // Iterate through the anchors and record the fixed points of the control
                    foreach (var anchor in LayoutFrame.Anchors)
                    {
                        // Get the control that the anchor is relative to
                        IFrameXmlControl anchorControl = GetAnchorControl(anchor);

                        // Make sure the anchor control is available
                        if (anchorControl != null)
                        {
                            // Get the anchored point
                            var anchorPoint = anchorControl.GetAnchorPoint(anchor);

                            // Convert anchor point to a point valid in our parent's client coordinate system
                            var clientAnchorPoint = this.Parent.PointToClient(anchorControl.PointToScreen(anchorPoint));

                            // Set the frame point to the anchored point
                            this.SetFramePoint(anchor.Point, clientAnchorPoint);
                        }
                    }
                }
            }

            suspendLayoutEvents = false;
        }

        /// <summary>
        /// Sets the virtual layout position.
        /// </summary>
        private void SetVirtualLayoutPosition()
        {
            if (this.Parent != null)
            {
                if (LayoutFrame.IsInVirtualState)
                {
                    // Move virtual controls to the middle of their parents (UiControl)
                    this.Left = this.Parent.Width / 2 - this.Width / 2;
                    this.Top = this.Parent.Height / 2 - this.Height / 2;
                }
                else
                {
                    this.Left = Int32.MinValue;
                    this.Top = Int32.MinValue;
                }
            }
        }
        
        /// <summary>
        /// Initializes the virtual layout position.
        /// </summary>
        public void InitializeVirtualLayoutPosition()
        {
            if (LayoutFrame.Virtual)
                SetVirtualLayoutPosition();
        }

        /// <summary>
        /// Clears the layout.
        /// </summary>
	    private void ClearLayout()
        {
            verticalPoints.Clear();
            horizontalPoints.Clear();

            // Set the frame's size to the explicit size, if available, empty otherwise
            // TODO: Implement handling RelDimension
            this.Size = LayoutFrame.SizeDimension != null &&
                        LayoutFrame.SizeDimension.IsAbsolute ? LayoutFrame.SizeDimension.ToSize() : new Size();
        }

		/// <summary>
		/// </summary>
		/// <param name="e">An <see cref="T:System.EventArgs"/> that contains the event data.</param>
        protected override void OnResize(EventArgs e)
        {
            base.OnResize(e);

            if (!suspendLayoutEvents)
            {
                this.UpdateAnchors(false);
                this.UpdateSize(true);
            }
        }

        /// <summary>
        /// Raises the <see cref="E:System.Windows.Forms.Control.Move"/> event.
        /// </summary>
        /// <param name="e">An <see cref="T:System.EventArgs"/> that contains the event data.</param>
        protected override void OnMove(EventArgs e)
        {
			base.OnMove(e);

			if(!suspendLayoutEvents)
			{
				this.UpdateSize(false);
				this.UpdateAnchors(true);
			}
        }

        /// <summary>
        /// Sets a frame point of the <see cref="LayoutFrameControl"/>.
        /// </summary>
        /// <param name="framePoint">The frame point to set.</param>
        /// <param name="point">The coordinates of the point.</param>
        private void SetFramePoint(FramePoint framePoint, Point point)
        {
            // Set the HorizontalPoint
            switch (framePoint)
            {
                case FramePoint.TopLeft:
                case FramePoint.Left:
                case FramePoint.BottomLeft:
                    this.SetHorizontalPoint(AxisPoint.Near, point.X);
                    break;
                case FramePoint.Top:
                case FramePoint.Center:
                case FramePoint.Bottom:
                    this.SetHorizontalPoint(AxisPoint.Center, point.X);
                    break;
                case FramePoint.TopRight:
                case FramePoint.Right:
                case FramePoint.BottomRight:
                    this.SetHorizontalPoint(AxisPoint.Far, point.X);
                    break;
                default:
                    throw new ArgumentException("Unknown FramePoint.", "framePoint");
            }

            // Set the VerticalPoint
            switch (framePoint)
            {
                case FramePoint.TopLeft:
                case FramePoint.Top:
                case FramePoint.TopRight:
                    this.SetVerticalPoint(AxisPoint.Near, point.Y);
                    break;
                case FramePoint.Left:
                case FramePoint.Center:
                case FramePoint.Right:
                    this.SetVerticalPoint(AxisPoint.Center, point.Y);
                    break;
                case FramePoint.BottomLeft:
                case FramePoint.Bottom:
                case FramePoint.BottomRight:
                    this.SetVerticalPoint(AxisPoint.Far, point.Y);
                    break;
                default:
                    throw new ArgumentException("Unknown FramePoint.", "framePoint");
            }
        }

        /// <summary>
        /// Sets the horizontal point.
        /// </summary>
        /// <param name="axisPoint">The axis point.</param>
        /// <param name="value">The value.</param>
        private void SetHorizontalPoint(AxisPoint axisPoint, int value)
        {
            // Store the horizontal point
            horizontalPoints[axisPoint] = value;

            // If more than one horizontal point is specified, calculate width
            if (horizontalPoints.Count > 1)
                this.Width = GetImplicitSpan(horizontalPoints);

            // Set Left using the AxisPoint
            switch (axisPoint)
            {
                case AxisPoint.Near:
                    this.Left = value;
                    break;
                case AxisPoint.Center:
                    this.Left = value - this.Width / 2;
                    break;
                case AxisPoint.Far:
                    this.Left = value - this.Width;
                    break;
            }
        }

        /// <summary>
        /// Sets the vertical point.
        /// </summary>
        /// <param name="axisPoint">The axis point.</param>
        /// <param name="value">The value.</param>
        private void SetVerticalPoint(AxisPoint axisPoint, int value)
        {
            // Set the horizontal point
            verticalPoints[axisPoint] = value;

            // If more than one vertical point is specified, calculate height
            if (verticalPoints.Count > 1)
                this.Height = GetImplicitSpan(verticalPoints);

            // Set Top using the AxisPoint
            switch (axisPoint)
            {
                case AxisPoint.Near:
                    this.Top = value;
                    break;
                case AxisPoint.Center:
                    this.Top = value - this.Height / 2;
                    break;
                case AxisPoint.Far:
                    this.Top = value - this.Height;
                    break;
            }
        }

        /// <summary>
        /// Gets the anchor control.
        /// </summary>
        /// <param name="anchor">The anchor.</param>
        /// <returns></returns>
        private IFrameXmlControl GetAnchorControl(Anchor anchor)
        {
            var control = this as IFrameXmlControl;
            return String.IsNullOrEmpty(anchor.RelativeTo) ? this.Parent as IFrameXmlControl :
                                                             this.Container.FindFrameXmlControlByName(control.Parent.ExpandName(anchor.RelativeTo));
        }

        /// <summary>
        /// Gets the implicit span.
        /// </summary>
        /// <param name="points">The points.</param>
        /// <returns></returns>
        private static int GetImplicitSpan(IDictionary<AxisPoint, int> points)
        {
            if (points.ContainsKey(AxisPoint.Near) && points.ContainsKey(AxisPoint.Far))
                return points[AxisPoint.Far] - points[AxisPoint.Near];

            if (points.ContainsKey(AxisPoint.Center))
            {
                if (points.ContainsKey(AxisPoint.Near))
                    return (points[AxisPoint.Center] - points[AxisPoint.Near]) * 2;
                if (points.ContainsKey(AxisPoint.Far))
                    return (points[AxisPoint.Far] - points[AxisPoint.Center]) * 2;
            }

            throw new InvalidOperationException("The points cannot implicitly define the span of the axis.");
        }

		/// <summary>
		/// Updates the anchors.
		/// </summary>
		/// <param name="raisePropertyChanged">if set to <c>true</c> [raise property changed].</param>
        protected void UpdateAnchors(bool raisePropertyChanged)
        {
            // Iterate through the anchors and refresh their Offsets
            if (LayoutFrame.Anchors != null)
            {
                foreach (var anchor in LayoutFrame.Anchors)
                {
                    // Get the control we are anchored to
                    var anchorControl = this.GetAnchorControl(anchor);
                    if (anchorControl != null)
                    {
                        // Get the frame point
                        var framePoint = this.GetFramePoint(anchor.Point);

                        // Get the frame point of the anchored control
                        var anchorPoint = anchorControl.GetAnchorPoint(anchor, true);

                        // Translate the anchorPoint to the client coordinate system
                        var clientAnchorPoint = this.PointToClient(anchorControl.PointToScreen(anchorPoint));

                        // Calculate the new offset
                    	anchor.Offset = new Dimension
                    	                	{
                    	                		X = framePoint.X - clientAnchorPoint.X,
                    	                		Y = clientAnchorPoint.Y - framePoint.Y
                        };
                    }
                }

                if (raisePropertyChanged)
                    this.OnPropertyChanged(new PropertyChangedEventArgs("Anchors"));
            }
        }

		/// <summary>
		/// Updates the size.
		/// </summary>
		/// <param name="raisePropertyChanged">if set to <c>true</c> [raise property changed].</param>
        protected void UpdateSize(bool raisePropertyChanged)
        {
            if (this.LayoutFrame.SizeDimension != null)
            {
                if (this.LayoutFrame.SizeDimension.IsRelative)
                    throw new NotImplementedException("Relative size dimension cannot be updated.");

                if (this.LayoutFrame.SizeDimension.IsAbsolute)
                {
                    if (this.LayoutFrame.SizeDimension.AbsDimension != null)
                    {
                        // Update the size of the LayoutFrame
                        this.LayoutFrame.SizeDimension.AbsDimension = new AbsDimension { X = this.Width, Y = this.Height };                        
                    }
                    else
                    {
                        this.LayoutFrame.SizeDimension.X = this.Width;
                        this.LayoutFrame.SizeDimension.Y = this.Height;
                    }
                }

                if (raisePropertyChanged)
                    this.OnPropertyChanged(new PropertyChangedEventArgs("SizeDimension"));
            }
        }
	}
}
